package mygame;

import com.jme3.export.Savable;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import com.jme3.terrain.geomipmap.TerrainQuad;

/**
 * Custom control class for steering the plane manually.
 * @author aicher
 * @version 0.0.2
 */
public class PlaneBasicControl extends AbstractControl implements Savable, Cloneable {
    /**
     * Vector pointing to the plane movement direction. The length is the speed of 
     * the plane
     */
    private Vector3f planeSpeedDirection = new Vector3f(0f, 0f, 0f);
    
    /**
     * Vector of length 1 in the X direction of the plane
     */
    public Vector3f planeAxisX = new Vector3f(0f, 0f, 0f);
    
    /**
     * Vector of length 1 in the Y direction of the plane
     */
    public Vector3f planeAxisY = new Vector3f(0f, 0f, 0f);
    
    /**
     * Vector of length 1 in the Z direction of the plane
     */
    public Vector3f planeAxisZ = new Vector3f(0f, 0f, 0f);
    
    /**
     * 2D position (like on a map)
     */
    private Vector2f planePosOnMap = new Vector2f(0f, 0f);
    
    /**
     * Terrain for calculating the height over ground
     */
    private TerrainQuad terrain = null;
    
    /**
     * Calculated value of height over the terrain ground
     */
    public float heightOverGround = 0f;
    
    /**
     * Value between 0 (no acceleration) and 1 (maximum acceleration)
     */
    private float throttle = 0f;
    
    /**
     * Constant value for each plane according to the class "PlaneBehaviour". This will be
     * taken from the instance of "MyPlane" and was realized for an easier access to the
     * plane behaviour value.
     */
    private int planeBehaviour = 0;
    
    /**
     * Central value for the plane position. This is the position of the reference
     * node of the plane
     */
    private Vector3f planePosition = null;
    
    /**
     * As soon as the plane touches ground, this value will be set to true. This is
     * necessary in order to switch between the aerodynamic behaviour in the air vs.
     * the plane behaviour of a plane on the ground (e.g. on the ground it will  not roll).
     */
    public boolean planeOnGround = false;
    
    /**
     * Speed value in X direction - which will be the plane wing.
     */
    public float speedInX = 0f;
    
    /**
     * Speed value in Y direction.
     */
    public float speedInY = 0f;
    
    /**
     * Speed value in Z direction - which will be the plane axis.
     */
    public float speedInZ = 0f;
    
    /**
     * Speed of the pich change of the plane
     */
    private float pitchValue = 0;
    
    /**
     * True, if the keyboard has sent a pitch up command 
     */
    private boolean doPitchUp = false;
    
    /**
     * True, if the keyboard has sent a pitch down command
     */
    private boolean doPitchDown = false;
    
    /**
     * Speed of the roll change of the plane
     */
    private float rollValue = 0;
    
    /**
     * True, if the keyboard has sent a roll left command 
     */
    private boolean doRollLeft = false;
    
    /**
     * True, if the keyboard has sent a roll right command
     */
    private boolean doRollRight = false;
    
    /**
     * Speed of the yaw change of the plane
     */
    private float yawValue = 0;
    
    /**
     * True, if the keyboard has sent a yaw left command 
     */
    private boolean doYawLeft = false;
    
    /**
     * True, if the keyboard has sent a yaw right command
     */
    private boolean doYawRight = false;
    
    /**
     * empty constructor - as described in the JMonkeyEngine tutorial
     */
    public PlaneBasicControl() {}
    
    /**
     * Setter of the throttle value
     * @param throttle 
     */
    public void setThrottle(float throttle) {
        this.throttle = throttle;
    }
    
    /**
     * All values that can be calculated centrally and reused afterwards
     * are handled here. Call this method in the beginning of the update loop 
     */
    private void calculateHelperValues() {
        this.planeAxisX = spatial.getLocalRotation().mult(Vector3f.UNIT_X);
        this.planeAxisY = spatial.getLocalRotation().mult(Vector3f.UNIT_Y);
        this.planeAxisZ = spatial.getLocalRotation().mult(Vector3f.UNIT_Z);
        this.speedInX = this.planeAxisX.dot(this.planeSpeedDirection);
        this.speedInY = this.planeAxisY.dot(this.planeSpeedDirection);
        this.speedInZ = this.planeAxisZ.dot(this.planeSpeedDirection);
        
        this.planePosOnMap.x = this.planePosition.x;
        this.planePosOnMap.y = this.planePosition.z;
        this.heightOverGround = this.planePosition.y - terrain.getHeight(this.planePosOnMap);
    }
    
    /**
     * Increases the speed based on the throttle and adapts the position accordingly.
     * @param tpf 
     */
    public void applyForces(float tpf) {
        this.planeSpeedDirection.addLocal(this.planeAxisZ
                .mult(PlaneBehaviour.PLANE_POWER[this.planeBehaviour] * throttle * tpf));
        
        // apply upswing
        this.planeSpeedDirection.addLocal(planeAxisY.mult(PlaneBehaviour.UPSWING_AIR[this.planeBehaviour]
                * tpf * this.speedInZ));
        
        // apply gravity
        this.planeSpeedDirection.y -= Main.GRAVITY_VALUE * tpf;
        
        // apply wind resistance
        this.planeSpeedDirection.subtractLocal(
                planeAxisX.mult(PlaneBehaviour.WIND_RESISTANCE_X[this.planeBehaviour] * this.speedInX * tpf));
        this.planeSpeedDirection.subtractLocal(
                planeAxisY.mult(PlaneBehaviour.WIND_RESISTANCE_Y[this.planeBehaviour] * this.speedInY * tpf));
        this.planeSpeedDirection.subtractLocal(
                planeAxisZ.mult(PlaneBehaviour.WIND_RESISTANCE_Z[this.planeBehaviour] * this.speedInZ * tpf));
        
        // move plane position according to speed
        this.planePosition.addLocal(planeSpeedDirection.mult(tpf));
    }
    
    /**
     * Places the plane towards the planePosition vector
     */
    public void movePlaneNode() {
        spatial.setLocalTranslation(planePosition);
    }
    
    /**
     * Constructor for the "normal" usage. Here the relevant reference values
     * will be set.
     * @param planeBehaviour For accessing the right constants in "PlaneBehaviour"
     */
    public PlaneBasicControl(int planeBehaviour, TerrainQuad terrain) {
        this.planeBehaviour = planeBehaviour;
        this.terrain = terrain;
    }
    
    /**
     * For pitching the plane. The plane pitch speed will be ramped up according
     * to the PITCH_INCREASE_SPEED. However, if the plane is on the ground the
     * pitch will only be possible, if the speedInZ is higher than the threshold.
     * @param delta Target value of the pitch speed
     * @param tpf 
     */
    public void pitchPlane(float delta, float tpf) {
        if (!this.planeOnGround || (this.speedInZ > PlaneBehaviour.STOP_PITCH_SPEED[this.planeBehaviour])) {
            if ((delta > 0) && (pitchValue < delta)) {
                pitchValue += tpf * PlaneBehaviour.PITCH_INCREASE_SPEED[this.planeBehaviour];
            } 
            if ((delta < 0) && (pitchValue > delta)) {
                pitchValue -= tpf * PlaneBehaviour.PITCH_INCREASE_SPEED[this.planeBehaviour];
            } 
        }
    }
    
    /**
     * roll the plane if it is not on the ground. The concept is that the method
     * gets a value, which is the target for the roll speed. The true roll value
     * will be increased in every cylce until the target is reached. This is done
     * in order to create a smooth roll behaviour.
     *
     * @param delta Delta value of angle change
     * @param tpf tpf value
     */
    public void rollPlane(float delta, float tpf) {
        if (!planeOnGround) {
            if ((delta > 0) && (rollValue < delta)) {
                rollValue += tpf * PlaneBehaviour.ROLL_INCREASE_SPEED[this.planeBehaviour];
            }
            if ((delta < 0) && (rollValue > delta)) {
                rollValue -= tpf * PlaneBehaviour.ROLL_INCREASE_SPEED[this.planeBehaviour];
            }
        }
    }
    
    
    /**
     * yaw the plane if it has a minimum of speed. The concept is that the method
     * gets a value, which is the target for the yaw speed. The true yaw value
     * will be increased in every cylce until the target is reached. This is done
     * in order to create a smooth yaw behaviour.
     *
     * @param delta Delta value of angle change
     * @param tpf tpf value
     */
    public void yawPlane(float delta, float tpf) {
        if (speedInZ > PlaneBehaviour.STOP_YAW_SPEED[this.planeBehaviour]) {
            if ((delta > 0) && (yawValue < delta)) {
                yawValue += tpf * PlaneBehaviour.YAW_INCREASE_SPEED[this.planeBehaviour];
            }
            if ((delta < 0) && (rollValue > delta)) {
                yawValue -= tpf * PlaneBehaviour.YAW_INCREASE_SPEED[this.planeBehaviour];
            }
        }
    }      
    
    @Override
    protected void controlUpdate(float tpf) {
        if (spatial != null) {
            calculateHelperValues();
            
            // all manipulation will be done here
            if (this.doPitchDown) {
                this.pitchPlane(PlaneBehaviour.PITCH_DELTA_KEYB[this.planeBehaviour], tpf);
            } else if (this.doPitchUp) {
                this.pitchPlane(-PlaneBehaviour.PITCH_DELTA_KEYB[this.planeBehaviour], tpf);
            }
            
            if (this.doRollLeft) {
                this.rollPlane(-PlaneBehaviour.ROLL_DELTA_KEYB[this.planeBehaviour], tpf);
            } else if (this.doRollRight) {
                this.rollPlane(PlaneBehaviour.ROLL_DELTA_KEYB[this.planeBehaviour], tpf);
            } 
            
            if (this.doYawLeft) {
                this.yawPlane(PlaneBehaviour.YAW_DELTA_KEYB[this.planeBehaviour], tpf);
            } else if (this.doYawRight) {
                this.yawPlane(-PlaneBehaviour.YAW_DELTA_KEYB[this.planeBehaviour], tpf);
            }
            
            this.turnPlaneAxis(tpf);
            this.applyForces(tpf);
           
        }
    }
    
    /**
     * For turning the plane in the three axis (pitch -> X-axis, roll -> Z-axis
     * and yaw-> Y-axis). If the controll methods pitchPlane, rollPlane and yawPlane
     * will not be called, this method will decrease the turn speeds towards 0 based on
     * the "TURN_RESET_SPEED" parameter.
     * @param tpf 
     */
    private void turnPlaneAxis(float tpf) {
        // insert the backforce to the turn values
        pitchValue -= pitchValue * PlaneBehaviour.TURN_RESET_SPEED[this.planeBehaviour] * tpf;
        rollValue -= rollValue * PlaneBehaviour.TURN_RESET_SPEED[this.planeBehaviour] * tpf;
        yawValue -= yawValue * PlaneBehaviour.TURN_RESET_SPEED[this.planeBehaviour] * tpf;
        
        // rotate the plane
        spatial.rotate(pitchValue * tpf, yawValue * tpf, rollValue * tpf);
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        // currently not used
    }
    
    @Override
    public void setSpatial(Spatial theSpatial) {
        super.setSpatial(theSpatial);
        
        // any setup will be done here
        this.planePosition = theSpatial.getLocalTranslation().clone();
    }
    
    @Override
    public Control cloneForSpatial(Spatial theSpatial) {
        final PlaneBasicControl control = new PlaneBasicControl(this.planeBehaviour, this.terrain);
        // do all initialization here (only for values that are not part of the constructor
        // and that are not calculated during the flight
        control.setSpatial(theSpatial);
        
        return control;
    }
    
    /**
     * For keyboard controll - the pitch up key was pressed or released
     * @param value true, for key pressed, false for key released
     */
    public void setPitchUp(boolean value) {
        this.doPitchUp = value;
    }
    
    /**
     * For keyboard controll - the pitch down key was pressed or released
     * @param value true, for key pressed, false for key released
     */
    public void setPitchDown(boolean value) {
        this.doPitchDown = value;
    }
    
    /**
     * For keyboard controll - the roll left key was pressed or released
     * @param value true, for key pressed, false for key released
     */
    public void setRollLeft(boolean value) {
        this.doRollLeft = value;
    }
    
    /**
     * For keyboard controll - the roll right key was pressed or released
     * @param value true, for key pressed, false for key released
     */
    public void setRollRight(boolean value) {
        this.doRollRight = value;
    }
    
    /**
     * For keyboard controll - the yaw left key was pressed or released
     * @param value true, for key pressed, false for key released
     */
    public void setYawLeft(boolean value) {
        this.doYawLeft = value;
    }
    
    /**
     * For keyboard controll - the yaw right key was pressed or released
     * @param value true, for key pressed, false for key released
     */
    public void setYawRight(boolean value) {
        this.doYawRight = value;
    }
    
}
